﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Text.RegularExpressions;

namespace Bettzueche.Dampfross
{
    /// <summary>
    /// 
    /// </summary>
    /// <example>
    /// Experiment experiment = new Experiment();
    /// bool loadSucc = experiment.LadeTestUmgebung(Experiment.Karten.Map9x10_2);
    /// if (!loadSucc) { ;/* handle failure*/ }
    /// bool initSucc = experiment.SetzteLernparameter(alpha: 0.25, epsilon: 0.01);
    /// experiment.StarteExperiment(episodes: 300, passes: 5);
    /// // Analyse der Werte in Experimente\exp_yyMMdd_HH-mm-ss.csv
    /// </example>
    class Experiment
    {
        public enum Karten
        {
            Map9x10_1, Map9x10_2
        }

        protected static readonly string[] _MapFilenames = { @"Karten\hexMap9x10-01.bin", @"Karten\hexMap9x10-02.bin" };
        protected static readonly string[] _ZidFilenames = { @"Karten\zIDs9x10-01.txt", @"Karten\zIDs9x10-02.txt" };
        protected static readonly string[] _SollFilenames = { @"Karten\Sollwerte9x10-01.csv", @"Karten\Sollwerte9x10-02.csv" };
        protected static readonly HexKoordinate[] _ZielKordinaten = { new HexKoordinate(3, 8), new HexKoordinate(3, 8) };
        /// <summary>
        /// Startkoordinaten, indiziert nach <c>Karten</c>-Typ (enum).
        /// Die Koords haben jeweils alle dieselben Minimalkosten zum Ziel
        /// </summary>
        protected static readonly HexKoordinate[][] _StartKoordinaten = 
        {
            new HexKoordinate[]{ // map9x10-01, Kosten=13
                new HexKoordinate(-1,4), new HexKoordinate(1,4), new HexKoordinate(4,1),
                new HexKoordinate(7,1), new HexKoordinate(6,3) },
            new HexKoordinate[]{ // map9x10-02, Kosten=11
                new HexKoordinate(-1,5), new HexKoordinate(-1,7), new HexKoordinate(-2,8),
                new HexKoordinate(4,1), new HexKoordinate(5,1), new HexKoordinate(7,1) }
        };



        #region Fields & Properties

        protected Karte _umwelt;
        protected Agent _agent;
        protected int _maxStepsPerEpisode;
        /// <summary>Aktuelle Startkoordinaten, für die gerade geladene Karte. Wird mit LadeTestUmgebung() gesetzt.</summary>
        protected HexKoordinate[] _startKoords;

        /// <summary>
        /// Anzahl Episoden pro Durchlauf. Standard = 500.
        /// </summary>
        /// <value>
        /// The episoden per pass.
        /// </value>
        public int EpisodenPerPass { get; set; }

        /// <summary>
        /// Innerhalb eines Durchlaufs, die Anzahl Episoden, die zu einem Mittelwert zusammengefasst werden.
        /// Standard = 10; Sollte ganzzahlig durch Gesamtzahl Episoden teilbar sein.
        /// </summary>
        /// <value>
        /// The episodes per bundle.
        /// </value>
        public int EpisodesPerBundle { get; set; }

        /// <summary>
        /// Anzahl Durchläufe je Experiment. Standard = 10.
        /// </summary>
        /// <value>
        /// The passes.
        /// </value>
        public int Passes { get; set; }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="Experiment"/> class.
        /// </summary>
        public Experiment() {
            EpisodenPerPass = 500;
            EpisodesPerBundle = 10;
            Passes = 10;
        }

        /// <summary>
        /// Lädt zur spezfizerten Umwelt die Karte und Sollwerte und setzt den (oder einen neuen Agenten) in die Welt.
        /// Hinweis: Beendet automatisch eine Lernepisode durch Agent.Clear();
        /// </summary>
        /// <param name="karte">Typ der Umgebung</param>
        /// <returns>true, wenn Initialisierung erfolgreich; false sonst.</returns>
        public bool LadeTestUmgebung(Karten karte) {
            int kartenNr = (int)karte;
            var hexKarte = Karte.LoadMapFromFile(_MapFilenames[kartenNr]);
            if (hexKarte == null) {
                return false;
            }
            Karte map = new Karte(hexKarte);
            map.Ziel = _ZielKordinaten[kartenNr];
            int[] zIds = LadeZIDs(_ZidFilenames[kartenNr]);
            double[][] sollwerte = LadeSollwerte(_SollFilenames[kartenNr]);
            if (sollwerte == null)
                return false;
            if (sollwerte.Length != zIds.Length) {
                Debug.WriteLine("Inkonsistente Daten. Anzahl von ZustandsIDs und Sollwert-Arrays stimmen nicht überein.");
                return false;
            }
            _maxStepsPerEpisode = 2 * zIds.Length; // doppelte Zustandszahl als Schrittlimit pro Epsiode

            _umwelt = map;
            if (_agent == null) {
                _agent = new Agent(_umwelt);
            }
            else
                _agent.Umwelt = _umwelt;
            _agent.SetStates(zIds);
            _agent.SetSollwerte(sollwerte);
            _agent.Reset();
            _startKoords = _StartKoordinaten[kartenNr];

            return true;
        }

        /// <summary>
        /// Setztes the lernparameter.
        /// </summary>
        /// <param name="alpha">optional: Lernrate. Standard = 0.5</param>
        /// <param name="lambda">optional: Spurzerfallsfaktor. Standard = 0.7</param>
        /// <param name="gamma">optional: Diskontierungsfaktor. Standard = 0.9</param>
        /// <param name="epsilon">optional: The Explorationsrate. Standard = 0.1</param>
        /// <param name="maxBackView">
        ///     optional: Maximale Anzahl Schritte, die ein Reward zurückreicht. Standard = 50<para>
        ///     Eine zum Spurzerfallsfaktor zusätliche Beschränkung der Lohnberechtigung in die Vergangenheit.</para>
        /// </param>
        /// <returns>true, wenn Initialisierung erfolgreich; false, wenn Agent oder Umwelt uninitialisiert.</returns>
        public bool SetzteLernparameter(double alpha = 0.5, double lambda = 0.7, double gamma = 0.9, double epsilon = 0.1, int maxBackView = 50) {
            if (_agent == null || _umwelt == null)
                return false;
            // Übernahme der Lernparameter und Beschränkung auf [0;1]
            _agent.Alpha = Math.Min(1.0, Math.Max(0.0, alpha));
            _agent.Lambda = Math.Min(1.0, Math.Max(0.0, lambda));
            _agent.Gamma = Math.Min(1.0, Math.Max(0.0, gamma));
            _agent.Epsilon = Math.Min(1.0, Math.Max(0.0, epsilon));
            _agent.MinBerechtigung = Math.Pow(_agent.Epsilon, maxBackView);
            return true;
        }


        /// <summary>
        /// Startet ein Experiment und speichert die Ergebnisse als csv im Ordner "Experimente".
        /// </summary>
        /// <param name="episodes">optional: Anzahl Episoden je Durchlauf. Standard = 500.</param>
        /// <param name="bundleCount">optional: Anzahl Episoden, deren Ergebnisse jeweils gemittelt (gebündelt) werden. Standard = 10.<para>
        ///             episodes sollte ganzzahlig durch bundleCount teilbar sein!</para></param>
        /// <param name="passes">optional: Anzahl Durchläufe in diesem Experiment. Standard = 10. Bei jedem Durchlauf wird
        ///             das Agentenwissen gelöscht.</param>
        public virtual void StarteExperiment(int episodes = 500, int bundleCount = 10, int passes = 10) {
            Random random = new Random();
            int bundles = episodes / bundleCount;
            double[][] meanCostsPerPass = new double[passes][];
            double[][] msePerPass = new double[passes][];
            // Je Durchlauf...
            for (int pass = 1; pass <= passes; pass++) {
                _agent.Clear(); // Erfahrung vor jedem Testdurchlauf löschen!
                
                meanCostsPerPass[pass-1] = new double[bundles];
                msePerPass[pass-1] = new double[bundles];
                int epInBundle = 0;
                int stepCnt = 0;
                double kosten = 0.0;
                double error = 0.0;
                int bundle = 0;
                double[] kostenArr = new double[bundleCount];
                double[] mseArr = new double[bundleCount];
                // Führe Lernepisoden aus:
                for (int episode = 1; episode <= episodes; episode++) {
                    int startNr = random.Next(_startKoords.Length);
                    _umwelt.ReSpawnAgent(_startKoords[startNr]); // Zufallsauswahl für Startfeld
                    _agent.Reset(); // vor jeder Epis. Historie + Reward zurücksetzen
                    stepCnt = _agent.RunEpisode(_maxStepsPerEpisode);
                    kosten = _agent.RewardTotal;
                    kostenArr[epInBundle] = kosten;
                    error = _agent.GetMSE();
                    mseArr[epInBundle] = error;
                    epInBundle++;
                    // nach je 10 Epsidoden, speichere Mittelwerte für reward und error
                    if (epInBundle == bundleCount) {
                        epInBundle = 0;
                        double meanCosts = kostenArr.Average();
                        double meanMSE = mseArr.Average();
                        meanCostsPerPass[pass-1][bundle] = meanCosts;
                        msePerPass[pass-1][bundle] = meanMSE;
                        bundle++;
                    }
                } // Ende des Durchlaufs
            } // Ende Experiment
            // Speichere Daten in csv
            StringBuilder sb = new StringBuilder();
            // --- HEADER ----
            sb.Append("Bundle;");
            for (int p = 1; p <= passes; p++) {
                sb.AppendFormat("Costs(P{0});MSE(P{0});",p);
            }
            sb.Append("Costs;MSE;");
            sb.AppendLine();

            for (int bundle = 1; bundle <= bundles; bundle++) {
                sb.Append(bundle + ";");
                double costSum = .0;
                double mseSum = .0;
                for (int pass = 0; pass < passes; pass++) {
                    sb.Append(meanCostsPerPass[pass][bundle-1] + ";");
                    sb.Append(msePerPass[pass][bundle-1] + ";");
                    costSum += meanCostsPerPass[pass][bundle-1];
                    mseSum += msePerPass[pass][bundle-1];
                }
                double costAvg = costSum / passes;
                double mseAvg = mseSum / passes;
                sb.AppendFormat("{0};{1};", costAvg, mseAvg);
                sb.AppendLine();
                
            }
            // Lernparameter speichern?
            sb.Append("Epsilon = " + _agent.Epsilon + "  ");
            sb.Append("Alpha = " + _agent.Alpha + "  ");
            sb.Append("Gamma = " + _agent.Gamma + "  ");
            sb.Append("Lambda = " + _agent.Lambda);
            sb.AppendLine();
            sb.Append("Episoden: " + episodes + "  ");
            sb.AppendLine("Ep. je Bündel: " + bundleCount);
            DateTime date = DateTime.Now;
            sb.AppendLine("Datum:" + date.ToShortDateString());
            // Datei (Name?)
            if (!Directory.Exists("Experimente")) {
                Directory.CreateDirectory("Experimente");
            }
            File.WriteAllText(@"Experimente\exp_" + date.ToString("yyMMdd_HH-mm-ss") + ".csv", sb.ToString());
        }




        #region public static

        /// <summary>
        /// Lädt die Hex-Kachel-Welt aus der Datei 'filename'.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns>HexMap, wenn Laden erfolgreich; null, wenn Fehler auftrat</returns>
        public static HexMapRectFlat<Kachel> LoadMapFromFile(string filename) {
            FileStream fs = new FileStream(filename, FileMode.Open);
            HexMapRectFlat<Kachel> map = null;
            try {
                BinaryFormatter formatter = new BinaryFormatter();
                map = (HexMapRectFlat<Kachel>)formatter.Deserialize(fs);
                return map;
            }
            catch (SerializationException e) {
                Debug.WriteLine("LoadMapFromFile: Failed to deserialize. Reason: " + e.Message);
                return null;
            }
            finally {
                fs.Close();
            }
        }

        /// <summary>
        /// Lädt die Zustands-IDs aus der angegebenen Datei und gibt sie als Array zurück.<para>
        /// Normalerweise solten die IDs reihenweise (zick-zack) sortiert sein und Stadt- wie innere
        /// Wasserfelder exklusive / ausgespart sein!
        /// </para>
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns>Zustandsnummern</returns>
        internal static int[] LadeZIDs(string filename) {
            List<int> idList = new List<int>();
            string idsStr = "";
            try {
                //var idLines = File.ReadLines(filename);
                idsStr = File.ReadAllText(filename);
            }
            catch (Exception e) {
                Debug.WriteLine("LadeZIDs: Failed to read file. Reason: " + e.Message);
                return null;
            }
            Regex rx = new Regex("\\d+\r$", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            MatchCollection matches = rx.Matches(idsStr);
            if (matches.Count > 0) {
                foreach (Match match in matches) {
                    string idStr = match.Value;
                    int id = Int32.Parse(idStr);
                    idList.Add(id);
                }
            }
            return idList.ToArray();
        }

        /// <summary>
        /// Lades the sollwerte.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns></returns>
        public static double[][] LadeSollwerte(string filename) {
            //List<int[]> idList = new List<int[]>();
            List<double[]> idList = new List<double[]>();
            string fullText = "";
            try {
                //var idLines = File.ReadLines(filename);
                fullText = File.ReadAllText(filename);
            }
            catch (Exception e) {
                Debug.WriteLine("LadeSollwerte: Failed to read file. Reason: " + e.Message);
                return null;
            }
            Regex rx = new Regex("(\\d+);(\\d+);(\\d+);(\\d+);(\\d+);(\\d+);\r$", RegexOptions.Multiline | RegexOptions.IgnoreCase);// 
            MatchCollection matches = rx.Matches(fullText);
            if (matches.Count > 0) {
                foreach (Match match in matches) {
                    //int[] soll_zArr = new int[6];
                    double[] soll_zArr = new double[6];
                    for (int i = 1; i <= 6; i++) {
                        //int soll_i = Int32.Parse(match.Groups[i].Value);
                        double soll_i = Double.Parse(match.Groups[i].Value);
                        soll_zArr[i-1] = soll_i;
                    }
                    idList.Add(soll_zArr);
                }
            }
            return idList.ToArray();
        }

        #endregion

    }
}
